package com.michael.demos.springboot.openapi.service;

import com.michael.demos.springboot.openapi.db.DB;
import com.michael.demos.springboot.openapi.entity.AppUser;
import com.michael.demos.springboot.openapi.exception.APIException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Service;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.Arrays;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;

/**
 * 类功能描述:
 * <pre>
 *   服务
 * </pre>
 *
 * @author Michael
 * @version 1.0
 * @date 2020/11/26 16:23
 */
@Service
public class OpenApiService {

    /** token 过期分钟数 */
    private static final long TOKEN_EXPIRE_MINUTES = 2 * 60L;
    /** nonce 过期分钟数 */
    private static final long NONCE_EXPIRE_MINUTES = 5L;

    private static final String SYMBOLS = "01234abcdefgABCDEFGhijklmnHIJKLMNopqrstOPQRSTuvwxyzUVWXYZ56789";

    private static final SecureRandom SECURE_RANDOM = new SecureRandom();

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    public AppUser get(String appKey) {
        return DB.getAppUser(appKey);
    }

    /** 生成用户数据 */
    public AppUser genOne() {
        AppUser user = new AppUser();
        user.setAppKey(genCode(6));
        user.setAppSecret(genCode(6));
        user.setSecretKey(genCode(64));

        DB.putAppUser(user);
        return user;
    }

    /** 用户调用API时需要先获取 TOKEN */
    public String getToken(String appKey, String appSecret) {
        // TOKEN 获取次数必要时需要做限制
        AppUser appUser = DB.getAppUser(appKey);
        if (appUser == null) {
            throw new APIException(ErrorCode.USER_NONE);
        }

        if (appUser.getAppSecret().equals(appSecret)) {
            // 用户名密码校验成功
            String token = genCode(64);

            redisTemplate.opsForValue().set(token, appUser, TOKEN_EXPIRE_MINUTES, TimeUnit.MINUTES);

            return token;
        } else {
            throw new APIException(ErrorCode.WRONG_SECRET);
        }
    }

    /** 检验请求合法性 */
    public boolean checkOk(Map<String, String> paramMap) {

        // 公用参数
        String token = paramMap.get("token");
        String timestamp = paramMap.get("timestamp");
        String nonce = paramMap.get("nonce");
        String sign = paramMap.get("sign");

        ValueOperations<String, Object> forValue = redisTemplate.opsForValue();

        // 1. 时间戳是否在有效期内
        checkTimestamp(timestamp);

        // 2. token 是否有效
        Object o = forValue.get(token);
        if (o == null) {
            throw new APIException(ErrorCode.TOKEN_EXPIRED);
        }

        AppUser appUser = (AppUser) o;
        String nonceKey = appUser.getAppKey() + "_NONCE_" + nonce;

        // 3. 过滤重复请求
        o = forValue.get(nonceKey);
        if (o != null) {
            throw new APIException(ErrorCode.REPEATED_REQ);
        }

        // 4. 签名检验
        checkSign(paramMap, appUser.getSecretKey(), sign);

        // 5. nonce 加入缓存
        forValue.set(nonceKey, 1, NONCE_EXPIRE_MINUTES, TimeUnit.MINUTES);

        return true;
    }

    /** 签名检验 */
    private void checkSign(Map<String, String> paramMap, String secretKey, String sign) {

        Set<String> keySet = paramMap.keySet();
        String[] keyArray = keySet.toArray(new String[0]);
        Arrays.sort(keyArray);

        StringBuilder sb = new StringBuilder();
        for (String k : keyArray) {
            if ("sign".equals(k)) {
                continue;
            }
            if (paramMap.get(k).trim().length() > 0) {
                // 参数值为空，则不参与签名
                sb.append(k).append("=").append(paramMap.get(k).trim()).append("&");
            }
        }
        sb.append("key=").append(secretKey);

        try {
            String serverSign = sha256(sb.toString(), secretKey);
            if (!serverSign.equals(sign)) {
                throw new APIException(ErrorCode.SIGN_ERROR);
            }
        } catch (Exception e) {
            throw new APIException(ErrorCode.SERVER_ERROR);
        }
    }

    public static String sha256(String data, String key) throws NoSuchAlgorithmException, InvalidKeyException {
        Mac sha256Hmac = Mac.getInstance("HmacSHA256");
        SecretKeySpec secretKeySpec = new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
        sha256Hmac.init(secretKeySpec);
        byte[] array = sha256Hmac.doFinal(data.getBytes(StandardCharsets.UTF_8));

        StringBuilder sb = new StringBuilder();
        for (byte item : array) {
            sb.append(Integer.toHexString((item & 0xFF) | 0x100), 1, 3);
        }
        return sb.toString().toUpperCase();
    }

    public void checkTimestamp(String timestamp) {

        try {
            long reqTimestamp = Long.parseLong(timestamp);
            long minTimestamp = LocalDateTime.now().minusMinutes(NONCE_EXPIRE_MINUTES)
                                             .toInstant(ZoneOffset.ofHours(8)).toEpochMilli();
            if (minTimestamp > reqTimestamp) {
                throw new APIException(ErrorCode.TIME_EXPIRED);
            }
        } catch (NumberFormatException e) {
            throw new APIException(ErrorCode.TIMESTAMP_ERROR);
        }
    }

    /** 用户最终需要调用的接口 */
    public String doA(String name) {
        return "请求成功 -> " + name;
    }

    /** 生成一个 size 位长度的随机数 */
    private String genCode(int size) {
        StringBuilder randomCode = new StringBuilder();
        for (int j = 0; j < size; j++) {
            randomCode.append(SYMBOLS.charAt(SECURE_RANDOM.nextInt(SYMBOLS.length())));
        }

        return randomCode.toString();
    }
}
